﻿using OpenTK;
using OpenTK.Graphics.OpenGL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LDLib
{
    /// <summary>
    /// Shader class, represents a shader that 
    /// consists of vertex and fragment shaders.
    /// No geometry shaders currently supported. :(
    /// </summary>
    public class Shader
    {
        /// <summary>
        /// Static access to the current shader
        /// </summary>
        public static Shader currentShader = null;

        /// <summary>
        /// Sets the shader to 0 and the current shader to null
        /// </summary>
        public static void ClearShader()
        {
            GL.UseProgram(0);
            currentShader = null;
        }

        /// <summary>
        /// Shader (program) ID
        /// </summary>
        protected int shaderId;
        public int ShaderID { get { return shaderId; } }

        /// <summary>
        /// Vertex shader ID
        /// </summary>
        protected int vertexId;

        /// <summary>
        /// Fragment shader ID
        /// </summary>
        protected int fragmentId;

        /// <summary>
        /// Dictionary of uniform locations <locationname, location>
        /// </summary>
        protected Dictionary<string, int> uniformLocations = new Dictionary<string, int>();

        /// <summary>
        /// String representation of the vertex shader source
        /// </summary>
        public string VertexSource { get; private set; }
        
        /// <summary>
        /// String representation of the fragment shader source
        /// </summary>
        public string FragmentSource { get; private set; }

        /// <summary>
        /// Constructs a new shader from textual representations
        /// </summary>
        /// <param name="vText">Vertex shader text</param>
        /// <param name="fText">Fragment shader text</param>
        public Shader(string vText, string fText)
        {
            this.VertexSource = vText;
            this.FragmentSource = fText;

            // Load shaders
            LoadVertexShader(vText);
            LoadFragmentShader(fText);

            // Create the shader program
            shaderId = GL.CreateProgram();
            GL.AttachShader(shaderId, vertexId);
            GL.AttachShader(shaderId, fragmentId);

            // Bind the attribute and data locations
            BindLocations();

            // Finally, link and validate the shader
            GL.LinkProgram(shaderId);

            int programStatusCode;
            GL.GetProgram(shaderId, ProgramParameter.ValidateStatus, out programStatusCode);
            if (programStatusCode == (int)All.False)
            {
                Console.WriteLine("Error creating shader program!");
                Console.WriteLine(GL.GetProgramInfoLog(shaderId));
            }

            GL.ValidateProgram(shaderId);

            SetupUniformLocations();
        }

        /// <summary>
        /// Loads the vertex shader from the given text string
        /// </summary>
        /// <param name="vText">The textual representation of the vertex shader</param>
        protected virtual void LoadVertexShader(string vText)
        {
            // Create and load vertex shader
            vertexId = GL.CreateShader(ShaderType.VertexShader);
            GL.ShaderSource(vertexId, vText);

            // Compile vertex shader
            GL.CompileShader(vertexId);
            string vertexLog = GL.GetShaderInfoLog(vertexId);
            int vertexStatusCode;
            GL.GetShader(vertexId, ShaderParameter.CompileStatus, out vertexStatusCode);
            if (vertexStatusCode != 1) Console.WriteLine("Error creating vertex shader: " + vertexLog);

        }

        /// <summary>
        /// Loads the fragment shader from the given text string
        /// </summary>
        /// <param name="fText">The textual representation of the vertex shader</param>
        protected virtual void LoadFragmentShader(string fText)
        {
            // Create and load fragment shader
            fragmentId = GL.CreateShader(ShaderType.FragmentShader);
            GL.ShaderSource(fragmentId, fText);

            // Compile fragment shader
            GL.CompileShader(fragmentId);
            string fragmentLog = GL.GetShaderInfoLog(fragmentId);
            int fragmentStatusCode;
            GL.GetShader(fragmentId, ShaderParameter.CompileStatus, out fragmentStatusCode);
            if (fragmentStatusCode != 1)
            {
                Console.WriteLine("Error creating fragment shader: " + fragmentLog);
            }
        }

        /// <summary>
        /// Binds the attribute and data locations of the shaders
        /// </summary>
        protected virtual void BindLocations()
        {

        }

        /// <summary>
        /// Stores the uniform locations for speedy access
        /// </summary>
        protected virtual void SetupUniformLocations()
        {

        }

        public virtual void AddUniforms(params string[] uniforms)
        {
            foreach(string u in uniforms)
            {
                uniformLocations.Add(u, GL.GetUniformLocation(shaderId, u));
            }
        }

        public virtual void AddAttribLocations(params string[] attribs)
        {
            int i = 0;
            foreach(string a in attribs)
            {
                GL.BindAttribLocation(shaderId, i++, a);
            }
        }

        public virtual void AddFragDataLocations(params string[] frags)
        {
            int i = 0;
            foreach(string f in frags)
            {
                GL.BindFragDataLocation(shaderId, i++, f);
            }
        }

        /// <summary>
        /// Sets OpenGL to use this shader
        /// </summary>
        public void Enable()
        {
            if (Shader.currentShader != this)
            {
                GL.UseProgram(shaderId);
                Shader.currentShader = this;
            }
        }

        /// <summary>
        /// Retrieves the uniform location of "name"
        /// </summary>
        /// <param name="name">The name of the uniform location</param>
        /// <returns>The uniform location, or -1 if not found</returns>
        public int GetUniformLocation(string name)
        {
            int loc = -1;
            if (uniformLocations.ContainsKey(name))
            {
                uniformLocations.TryGetValue(name, out loc);
                return loc;
            }
            else
            {
                throw new Exception(name + " not found in current shader!");
            }
        }

        /// <summary>
        /// Loads a four-component uniform value
        /// </summary>
        /// <param name="name">Name of the uniform</param>
        /// <param name="value">Value</param>
        internal void Set(string name, Vector4 value)
        {
            GL.Uniform4(GetUniformLocation(name), value);
        }

        /// <summary>
        /// Loads a three-component uniform value
        /// </summary>
        /// <param name="name">Name of the uniform</param>
        /// <param name="value">Value</param>
        internal void Set(string name, Vector3 value)
        {
            GL.Uniform3(GetUniformLocation(name), value);
        }

        /// <summary>
        /// Loads a 4x4 matrix uniform value
        /// </summary>
        /// <param name="name">Name of the uniform</param>
        /// <param name="value">Value</param>
        internal void Set(string name, Matrix4 value)
        {
            GL.UniformMatrix4(GetUniformLocation(name), false, ref value);
        }

        /// <summary>
        /// Loads an integer uniform value
        /// </summary>
        /// <param name="name">Name of the uniform</param>
        /// <param name="value">Value</param>
        internal void Set(string name, int value)
        {
            GL.Uniform1(GetUniformLocation(name), value);
        }

        /// <summary>
        /// Loads a floating point uniform value
        /// </summary>
        /// <param name="name">Name of the uniform</param>
        /// <param name="value">Value</param>
        internal void Set(string name, float value)
        {
            GL.Uniform1(GetUniformLocation(name), value);
        }
    }
}
